package com.zyl.blog.rbac.advice;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.json.JSONUtil;
import com.alibaba.nacos.shaded.com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.zyl.blog.common.config.SpelParser;
import com.zyl.blog.common.config.anno.SysLog;
import com.zyl.blog.common.domain.data.bean.Ip;
import com.zyl.blog.common.domain.data.bean.LyLog;
import com.zyl.blog.common.domain.data.vo.LogVO;
import com.zyl.blog.common.enums.OpenStatus;
import com.zyl.blog.common.exception.BlogException;
import com.zyl.blog.common.execute.LoggingVirtualThreadExecutor;
import com.zyl.blog.common.utils.Extreme.IpUtils;
import com.zyl.blog.common.utils.Extreme.LogUtil;
import com.zyl.blog.common.utils.Extreme.SnowIdUtil;
import com.zyl.blog.rbac.service.rbac.UserService;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import nl.bitwalker.useragentutils.Browser;
import nl.bitwalker.useragentutils.OperatingSystem;
import nl.bitwalker.useragentutils.UserAgent;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

/**
 * 系统操作日志切面处理器
 *
 * <p>本切面实现以下企业级功能：
 * 1. 基于注解的声明式日志记录
 * 2. 多维度的操作审计（业务类型、操作类型、操作结果）
 * 3. 全链路追踪支持（TraceID注入）
 * 4. 敏感数据自动脱敏
 * 5. 双写日志存储（DB + Console）
 * 6. 异步化处理提升性能
 * @version v1.0
 * @ProjectName: Personal_Blog_Server
 * @Package: com.zyl.rbac.advice
 * @ClassName: OpenLogAspect
 * @Description: 龙渊博客系统日志切面处理器
 * @Author FinalFantasy
 * @Date 2025/8/11-10:31
 */
@Aspect
@Component
@RequiredArgsConstructor
public class LyBlogAspect {
    private final UserService userService;
    //private final LyLogService logService;
    private final SpelParser spelParser;

    private static final String LOG_BASE_DIR = "/logs/blogLog/";
    private static final String LOG_FILE_PATTERN = "%s/log_%s.log";

    @Resource
    private IpUtils ipUtils;

    private static final String consoleTemplate = """
        \n======= 操作审计日志 =======
            TraceID        :  {}
            业务模块        :  {} [{}]
            操作类型        :  {} [{}]
            操作描述        :  {}
        【IP地理信息】
            IP地址         :  {}
            地理位置       :  {}
            大陆板块       :  {}
            邮政编码       :  {}
            行政编码       :  {}
            运营商家       :  {}
        【请求信息】
            请求方法       :  {}
            请求路径       :  {}
            执行方法       :  {}
            客户端信息     :  {} ({})
        【操作信息】
            操作人员       :  {}({})
            执行状态       :  {}
            接口耗时       :  {}ms
            异常信息       :  {}
        ============================
        """;

    /* 传统线程池配置（与业务线程池隔离） （备用）*/
    private static final ThreadPoolExecutor LOG_EXECUTOR_backup = new ThreadPoolExecutor(
            2,
            5,
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1000),
            new ThreadFactoryBuilder()
                    .setNameFormat("log-async-%d")
                    .setDaemon(true)
                    .setPriority(Thread.NORM_PRIORITY)
                    .setUncaughtExceptionHandler((t, e) ->
                            LogUtil.error("日志处理线程 {} 发生异常", null, t.getName(), e))
                    .build(),
            new ThreadPoolExecutor.CallerRunsPolicy() // 当队列满时让提交任务的线程执行，避免日志丢失
    );

    /* 虚拟线程池，自动管理线程生命周期，适合 IO 密集型任务 （JDK24） */
    private static final ExecutorService LOG_EXECUTOR = Executors.newVirtualThreadPerTaskExecutor();

    /**
     * 切点：所有标注了@SysLog注解的方法
     */
    @Pointcut("@annotation(com.zyl.blog.common.config.anno.SysLog)")
    public void logPointCut() {}

    /**
     * 环绕通知：记录操作日志
     * 执行流程：
     * 1. 构建日志上下文
     * 2. 执行目标方法
     * 3. 记录响应结果
     * 4. 异常捕获处理
     * 5. 日志持久化
     * 6. 控制台输出
     */
    @Around("logPointCut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        if (!(requestAttributes instanceof ServletRequestAttributes servletRequestAttributes)) {
            // 非HTTP环境直接执行方法，不记录日志
            return joinPoint.proceed();
        }

        HttpServletRequest request = servletRequestAttributes.getRequest();
        var method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        var sysLog = method.getAnnotation(SysLog.class);

        // 初始化日志对象
        LyLog lyLog = LyLog.builder().build();
        var userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
        var ipInfo = ipUtils.getGeoInfoFromRequest(request);

        try {

            // 构建基础日志信息
            buildBaseLogInfo(joinPoint, sysLog, lyLog, request, ipInfo, userAgent);

            // 记录请求参数
            if (sysLog.recordParams()) {
                recordRequestParams(joinPoint, lyLog);
            }

            // 执行目标方法
            Object result = joinPoint.proceed();

            // 记录成功状态和响应结果
            lyLog.setStatus(OpenStatus.SUCCESS.getCode());
            lyLog.setStatusEnum(OpenStatus.SUCCESS);
            if (sysLog.recordResult()) {
                recordResponseResult(result, lyLog);
            }

            return result;
        } catch (BlogException e) {
            // 处理业务异常
            handleBusinessException(e, lyLog);
            throw e;
        } catch (Exception e) {
            // 处理系统异常
            handleSystemException(e, lyLog);
            throw e;
        } finally {
            // 记录耗时和操作时间
            lyLog.setCostTime(System.currentTimeMillis() - startTime);
            lyLog.setOperationTime(LocalDateTime.now());

            // 异步处理日志存储和打印
            asyncProcessLog(lyLog, request, userAgent, ipInfo);
        }
    }

    /**
     * 构建基础日志信息
     */
    private void buildBaseLogInfo(
            ProceedingJoinPoint joinPoint,
            SysLog sysLog,
            LyLog lyLog,
            HttpServletRequest request,
            Ip ipInfo,
            UserAgent userAgent
    ) {
        /* 设置基础ID和追踪信息 */
        lyLog
                .setLogId(SnowIdUtil.getSnowflakeNextId())
                .setTraceId(UUID.randomUUID().toString())

        /* 设置业务和操作类型 */
                .setBusinessType(sysLog.businessType().getCode())
                .setBusinessTypeEnum(sysLog.businessType())
                .setOperationType(sysLog.operationType().getCode())
                .setOperationTypeEnum(sysLog.operationType())

        /* 解析操作描述（支持SPEL表达式） */
                .setOperationDesc(spelParser.parse(sysLog.value(), joinPoint))

        /* 设置请求相关信息 */
                .setRequestUri(request.getRequestURI())
                .setRequestMethod(request.getMethod())
                .setClientIp(ipInfo.getIp())
                .setIpLocation(ipInfo.getFullLocation())
                .setUserAgent(request.getHeader("User-Agent"))

        /* 详细记录UserAgent信息 */
                .setUserAgent(buildUserAgentInfo(userAgent));

        /* 设置方法签名 */
        var signature = (MethodSignature) joinPoint.getSignature();
        lyLog.setMethodSignature(buildMethodSignature(signature));

        /* 设置操作用户信息 */
        setOperatorInfo(lyLog);

        /* 记录IP详细信息到额外字段 */
        recordIpDetailsToExtraInfo(lyLog, ipInfo, userAgent);
    }

    /**
     * 构建用户代理信息字符串
     */
    private String buildUserAgentInfo(UserAgent userAgent) {
        if (userAgent == null) {
            return "未知客户端";
        }

        Browser browser = userAgent.getBrowser();
        OperatingSystem os = userAgent.getOperatingSystem();

        return "浏览器: " + browser.getName() + " " +
                "版本: " + (userAgent.getBrowserVersion() != null ?
                userAgent.getBrowserVersion().getVersion() : "未知") + "; " +
                "操作系统: " + os.getName() + "; " +
                "设备类型: " + getDeviceType(os) + "; ";
    }

    /**
     * 获取设备类型名称（基于 JDK 24 语法优化）
     */
    private String getDeviceType(OperatingSystem os) {
        // 利用 switch 表达式 + 模式匹配直接返回设备类型名称
        return switch (os.getDeviceType()) {
            case MOBILE -> "移动设备";
            case TABLET -> "平板设备";
            case COMPUTER -> "桌面设备";
            case GAME_CONSOLE -> "游戏主机";
            // 若未来扩展 DeviceType.SMART_TV，可直接在此处添加分支
            case UNKNOWN, DMR -> "未知设备";
        };
    }

    /**
     * 记录IP详细信息到额外字段
     */
    private void recordIpDetailsToExtraInfo(LyLog lyLog, Ip ipInfo, UserAgent userAgent) {
        Map<String, Object> extraInfo = lyLog.getExtraInfo() == null ? new HashMap<>() : lyLog.getExtraInfo();
        lyLog.setExtraInfo(extraInfo);

        // 记录IP详细信息
        Map<String, Object> ipDetails = getStringObjectMap(ipInfo);

        // 记录设备信息
        Map<String, Object> deviceInfo = new HashMap<>();
        if (userAgent != null) {
            deviceInfo.put("browser", userAgent.getBrowser().getName());
            deviceInfo.put("browserVersion", userAgent.getBrowserVersion() != null ?
                    userAgent.getBrowserVersion().getVersion() : "未知");
            deviceInfo.put("os", userAgent.getOperatingSystem().getName());
            deviceInfo.put("deviceType", getDeviceType(userAgent.getOperatingSystem()));
            deviceInfo.put("userAgentString", userAgent.toString());
        }

        extraInfo.put("ipDetails", ipDetails);
        extraInfo.put("deviceInfo", deviceInfo);
    }

    private static Map<String, Object> getStringObjectMap(Ip ipInfo) {
        Map<String, Object> ipDetails = new HashMap<>();
        // 基础IP信息
        ipDetails.put("ip", ipInfo.getIp());
        ipDetails.put("continent", ipInfo.getContinent()); // 大洲
        ipDetails.put("country", ipInfo.getCountry());     // 国家
        ipDetails.put("province", ipInfo.getProvince());   // 省份
        ipDetails.put("city", ipInfo.getCity());           // 城市
        ipDetails.put("district", ipInfo.getDistrict());   // 区县
        // 网络与编码信息
        ipDetails.put("zipcode", ipInfo.getZipcode());     // 邮政编码
        ipDetails.put("adcode", ipInfo.getAdcode());       // 行政区划编码
        ipDetails.put("owner", ipInfo.getOwner());         // 网络所有者
        ipDetails.put("isp", ipInfo.getIsp());             // 运营商
        // 完整地理位置描述
        ipDetails.put("fullLocation", ipInfo.getFullLocation());
        return ipDetails;
    }

    /**
     * 构建方法签名字符串
     */
    private String buildMethodSignature(MethodSignature signature) {
        String className = signature.getDeclaringType().getSimpleName();
        String methodName = signature.getName();
        String[] parameterTypes = Arrays.stream(signature.getParameterTypes())
                .map(Class::getSimpleName)
                .toArray(String[]::new);

        return String.format("%s.%s(%s)",
                className,
                methodName,
                String.join(",", parameterTypes));
    }

    /**
     * 设置操作用户信息
     */
    private void setOperatorInfo(LyLog lyLog) {
        if (StpUtil.isLogin()) {
            try {
                String loginId = StpUtil.getLoginId().toString();
                lyLog.setOperatorId(Long.parseLong(loginId));

                /* 异步获取用户详情，避免阻塞主流程 */
                /* 复制必要信息到局部，避免修改共享对象 */
                long operatorId = lyLog.getOperatorId();
                LOG_EXECUTOR.execute(() -> {
                    try {
                        LogVO logVO = userService.getLogVO(operatorId);

                        /* 使用线程安全的方式更新（若 LyLog 用 @Data 需加 @Synchronized 或用 AtomicReference） */
                        lyLog.setOperatorName(logVO.operatorName());
                        lyLog.setOperatorDept(logVO.operatorDept());
                    } catch (Exception e) {
                        LogUtil.error("获取用户日志信息失败", e);
                    }
                });
            } catch (Exception e) {
                LogUtil.warn("设置操作用户信息失败", e);
            }
        }
    }

    /**
     * 记录请求参数
     */
    private void recordRequestParams(ProceedingJoinPoint joinPoint, LyLog lyLog) {
        try {
            Object[] args = joinPoint.getArgs();
            String rawParams = JSONUtil.toJsonStr(args);
            lyLog.setRawParams(rawParams);

            // 构建结构化参数信息
            Map<String, Object> paramMap = new HashMap<>(args.length);
            String[] paramNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();

            for (int i = 0; i < args.length; i++) {
                /* 敏感参数过滤示例 */
                Object paramValue = maskSensitiveData(paramNames[i], args[i]);
                paramMap.put(paramNames[i], paramValue);
            }

            lyLog.setRequestParams(paramMap);
        } catch (Exception e) {
            LogUtil.warn("记录请求参数失败", e);
            lyLog.setRawParams("记录请求参数时发生异常: " + e.getMessage());
        }
    }

    /**
     * 敏感数据脱敏处理
     */
    private Object maskSensitiveData(String paramName, Object value) {
        if (value == null) {
            return null;
        }

        // 常见敏感字段脱敏
        String lowerParamName = paramName.toLowerCase();

        return switch (lowerParamName) {
            case String s when s.contains("password") || s.contains("pwd") -> "******";
            case String s when s.contains("mobile") || s.contains("phone") ->
                    value instanceof String phone ? phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2") : value;
            case String s when s.contains("idcard") || s.contains("id_card") ->
                    value instanceof String idCard ? idCard.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2") : value;
            case String s when s.contains("email") ->
                    value instanceof String email ? email.replaceAll("(\\w+)@(\\w+\\.\\w+)", "$1***@$2") : value;
            default -> value;
        };
    }

    /**
     * 记录响应结果
     */
    private void recordResponseResult(Object result, LyLog lyLog) {
        try {
            lyLog.setRawResult(JSONUtil.toJsonStr(result));
            lyLog.setResponseResult(result);
        } catch (Exception e) {
            LogUtil.warn("记录响应结果失败", e);
            lyLog.setRawResult("记录响应结果时发生异常: " + e.getMessage());
        }
    }

    /**
     * 处理业务异常
     */
    private void handleBusinessException(BlogException e, LyLog lyLog) {
        lyLog.setStatus(OpenStatus.BIZ_FAIL.getCode());
        lyLog.setStatusEnum(OpenStatus.BIZ_FAIL);
        lyLog.setErrorCode(String.valueOf(e.getCode()));
        lyLog.setErrorMsg(e.getMessage());
    }

    /**
     * 处理系统异常
     */
    private void handleSystemException(Exception e, LyLog lyLog) {
        lyLog.setStatus(OpenStatus.SYS_FAIL.getCode());
        lyLog.setStatusEnum(OpenStatus.SYS_FAIL);
        lyLog.setErrorCode("SYS_500");
        lyLog.setErrorMsg("系统内部错误: " + e.getMessage());

        /* 记录异常堆栈信息到额外信息 */
        List<String> stackTrace = Arrays.stream(e.getStackTrace())
                .map(StackTraceElement::toString)
                .collect(Collectors.toList());

        Map<String, Object> extraInfo = lyLog.getExtraInfo() == null ? new HashMap<>() : lyLog.getExtraInfo();
        extraInfo.put("stackTrace", stackTrace);
        lyLog.setExtraInfo(extraInfo);
    }

    /**
     * 异步处理日志（存储和打印）
     */
    private void asyncProcessLog(LyLog lyLog, HttpServletRequest request,
                                 UserAgent userAgent, Ip ipInfo) {
        LOG_EXECUTOR.execute(() -> {
            try {
                // 保存日志到数据库
                //logService.save(lyLog);

                // 打印控制台日志
                printConsoleLog(lyLog, request, userAgent, ipInfo);
            } catch (Exception e) {
                LogUtil.error("[BlogLog] 日志存储失败，降级到本地文件", e);
                writeLogToLocalFile(lyLog);
            }
        });
    }

    /**
     * 控制台打印日志信息
     */
    private void printConsoleLog(
            LyLog lyLog,
            HttpServletRequest request,
            UserAgent userAgent,
            Ip ipInfo
    ) {
        /* 获取浏览器和设备信息 使用 Optional 简化 null 判断 */
        String browser = Optional.ofNullable(userAgent)
                .map(UserAgent::getBrowser)
                .map(Browser::getName)
                .orElse("未知");
        String device = userAgent != null ? getDeviceType(userAgent.getOperatingSystem()) : "未知";

        // 组装日志参数
        Object[] params = {
                lyLog.getTraceId(),
                lyLog.getBusinessTypeEnum().getDesc(), lyLog.getBusinessType(),
                lyLog.getOperationTypeEnum().getDesc(), lyLog.getOperationType(),
                lyLog.getOperationDesc(),
                // IP地理信息参数
                ipInfo.getIp(),
                ipInfo.getFullLocation(),
                ipInfo.getContinent(),
                ipInfo.getZipcode(),
                ipInfo.getAdcode(),
                ipInfo.getIsp(),
                // 请求信息参数
                request.getMethod(),
                lyLog.getRequestUri(),
                lyLog.getMethodSignature(),
                browser, device,
                // 操作信息参数
                lyLog.getOperatorName(), lyLog.getOperatorId(),
                lyLog.getStatusEnum().getDesc(),
                lyLog.getCostTime(),
                lyLog.getErrorMsg() == null ? "无异常" : lyLog.getErrorMsg()
        };


        // 根据日志状态选择不同的日志级别
        if (OpenStatus.SUCCESS.getCode().equals(lyLog.getStatus())) {
            LogUtil.info(consoleTemplate, params);
        } else {
            LogUtil.error(consoleTemplate, null, params);
        }
    }

    /**
     * 数据库存储失败时写入本地文件
     */
    private void writeLogToLocalFile(LyLog lyLog) {
        try {
            String logDir = LOG_BASE_DIR + LocalDate.now();
            File dir = new File(logDir);
            if (!dir.exists() && !dir.mkdirs()) {
                LogUtil.error("[LyBlog] 创建日志目录失败: {}", null, logDir);
                return;
            }

            String fileName = String.format(LOG_FILE_PATTERN, logDir, lyLog.getStatus());
            String logContent = String.format("[%s] %s%n",
                    LocalDateTime.now(), JSONUtil.toJsonStr(lyLog));

            try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(
                    Paths.get(fileName), StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND
            )) {
                channel.write(ByteBuffer.wrap(logContent.getBytes(StandardCharsets.UTF_8)), channel.size(), null,
                        new CompletionHandler<Integer, Void>() {
                            @Override
                            public void completed(Integer result, Void attachment) {
                                try {
                                    channel.close();
                                } catch (IOException e) {
                                    LogUtil.error("[BlogLog] 本地文件写入失败", e);
                                }
                            }

                            @Override
                            public void failed(Throwable exc, Void attachment) {
                                LogUtil.error("[BlogLog] 本地文件写入失败", exc);
                            }
                        }
                );
            }
        } catch (IOException e) {
            LogUtil.error("[BlogLog] 本地文件写入失败", e);
        }
    }

    @Bean(destroyMethod = "close")
    public ExecutorService logExecutor() {
        return new LoggingVirtualThreadExecutor("log-virtual-pool");
    }

}
